﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.CodeDom.Compiler;

namespace dotGen
{
    public sealed class dotGenEngine
    {
        private readonly Dictionary<Type, List<BaseDirective>> _directives = new Dictionary<Type, List<BaseDirective>>();
        private readonly List<string> _scriptBlocks = new List<string>();

        public string Generate(string[] lines)
        {
            string template = LoadDirectivesAndGetTemplate(lines);
            template = LoadScriptBlocks(template);

            CodeDomProvider provider = CodeDomProvider.CreateProvider("c#");

            var compileParameters = new CompilerParameters
            {
                GenerateExecutable = false,
                GenerateInMemory = true,
                ReferencedAssemblies = { "mscorlib.dll", "System.dll", },
            };
            List<BaseDirective> assemblyDirectives;
            if (_directives.TryGetValue(typeof(AssemblyDirective), out assemblyDirectives))
            {
                foreach (AssemblyDirective assemblyDirective in assemblyDirectives)
                    compileParameters.ReferencedAssemblies.Add(assemblyDirective.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ?
                        assemblyDirective.Name : assemblyDirective.Name + ".dll");
            }

            string generatorCode = BuildGeneratorCode(template);
            CompilerResults compilerResults = provider.CompileAssemblyFromSource(compileParameters, GetBaseGeneratorCode(), generatorCode);

            Type generatorType = compilerResults.CompiledAssembly.GetType("Generator");
            MethodInfo generateMethod = generatorType.GetMethod("Generate");
            PropertyInfo nsProperty = generatorType.GetProperty("Namespace");

            object generator = Activator.CreateInstance(generatorType);
            nsProperty.SetValue(generator, "Jeevan.Generated", null);
            object resultObject = generateMethod.Invoke(generator, null);
            return resultObject.ToString();
        }

        private string LoadDirectivesAndGetTemplate(string[] lines)
        {
            int lineIdx;
            for (lineIdx = 0; lineIdx < lines.Length; lineIdx++)
            {
                string line = lines[lineIdx].Trim();
                if (line.Length == 0)
                    continue;

                Match match = DirectiveRE.Match(line);
                if (!match.Success)
                    return string.Join(Environment.NewLine, lines, lineIdx, lines.Length - lineIdx);

                string directiveName = match.Groups[DirectiveRE_Directive].Value;
                Type directiveType = Type.GetType(string.Format(DirectiveTypeNameFormat, directiveName));
                List<BaseDirective> directiveList;
                if (!_directives.TryGetValue(directiveType, out directiveList))
                {
                    directiveList = new List<BaseDirective>();
                    _directives.Add(directiveType, directiveList);
                }

                var directive = (BaseDirective)Activator.CreateInstance(directiveType);

                int attributeCount = match.Groups[DirectiveRE_Names].Captures.Count;
                for (int attributeIdx = 0; attributeIdx < attributeCount; attributeIdx++)
                {
                    string attributeName = match.Groups[DirectiveRE_Names].Captures[attributeIdx].Value;
                    string attributeValue = match.Groups[DirectiveRE_Values].Captures[attributeIdx].Value;
                    PropertyInfo property = directiveType.GetProperty(attributeName);
                    property.SetValue(directive, attributeValue, null);
                }
                directiveList.Add(directive);
            }
            return string.Empty;
        }

        private string LoadScriptBlocks(string template)
        {
            return ScriptBlockRE.Replace(template,
                match =>
                {
                    _scriptBlocks.Add(match.Groups[ScriptBlockRE_CodeBlock].Value);
                    return string.Empty;
                });
        }

        private static string GetBaseGeneratorCode()
        {
            using (Stream resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(BaseGeneratorResource))
            using (var reader = new StreamReader(resourceStream))
                return reader.ReadToEnd();
        }

        private string BuildGeneratorCode(string template)
        {
            var code = new StringBuilder();
            IncludeImports(code);
            code.AppendLine("public sealed class Generator : BaseGenerator");
            code.AppendLine("{");
            code.AppendLine("public override string Generate()");
            code.AppendLine("{");
            IncludeTemplate(code, template);
            code.AppendLine("}");
            IncludeProperties(code);
            foreach (string scriptBlock in _scriptBlocks)
                code.AppendLine(scriptBlock);
            code.AppendLine("}");
            return code.ToString();
        }

        private void IncludeImports(StringBuilder code)
        {
            List<BaseDirective> importDirectives;
            if (!_directives.TryGetValue(typeof(ImportDirective), out importDirectives))
                return;
            foreach (ImportDirective importDirective in importDirectives)
                code.AppendFormat("using {0};", importDirective.Namespace).AppendLine();
        }

        //private static void IncludeTemplate(StringBuilder code, string template)
        //{
        //    MatchCollection matches = InlineBlockRE.Matches(template);
        //    int startPos = 0;
        //    foreach (Match match in matches)
        //    {
        //        string text = template.Substring(startPos, match.Index - startPos).Replace(@"""", @"""""");
        //        if (text.Length > 0)
        //            code.AppendFormat(@"Write(@""{0}"");", text).AppendLine();

        //        string inlineScript = match.Groups[InlineBlockRE_Script].Value.Trim();
        //        if (match.Groups[InlineBlockRE_Assignment].Length > 0)
        //            code.AppendFormat(@"Write({0});", inlineScript).AppendLine();
        //        else
        //            code.AppendLine(inlineScript);
        //        startPos = match.Index + match.Length;
        //    }
        //    if (matches.Count > 0)
        //    {
        //        Match lastMatch = matches[matches.Count - 1];
        //        string endText = template.Substring(lastMatch.Index + lastMatch.Length);
        //        if (endText.Length > 0)
        //            code.AppendFormat(@"Write(@""{0}"");", endText).AppendLine();
        //    }
        //    code.AppendLine(@"return ToString();");
        //}

        private static void IncludeTemplate(StringBuilder code, string template)
        {
            int startPos = 0;
            int blockStart = template.IndexOf("<%", startPos);
            while (blockStart >= 0)
            {
                string text = template.Substring(startPos, blockStart - startPos).
                    Replace(@"""", @"""""").
                    Replace("{", "{{").
                    Replace("}", "}}").
                    Replace(Environment.NewLine, "{0}");
                if (text.Length > 0)
                    code.AppendFormat(@"Write(@""{0}"", Environment.NewLine);", text).AppendLine();

                int blockEnd = template.IndexOf("%>", blockStart);
                if (blockEnd < 0)
                    throw new Exception("Matching end of inline block not found");

                if (template[blockStart + 2] == '=')
                {
                    string inlineBlock = template.Substring(blockStart + 3, blockEnd - blockStart - 3).Trim();
                    code.AppendFormat(@"Write({0});", inlineBlock).AppendLine();
                }
                else
                {
                    string inlineBlock = template.Substring(blockStart + 2, blockEnd - blockStart - 2).Trim();
                    code.AppendLine(inlineBlock);
                }

                startPos = blockEnd + 2;
                blockStart = template.IndexOf("<%", startPos);
            }

            string endText = template.Substring(startPos).
                Replace(@"""", @"""""").
                Replace("{", "{{").
                Replace("}", "}}").
                Replace(Environment.NewLine, "{0}");
            if (endText.Length > 0)
                code.AppendFormat(@"Write(@""{0}"", Environment.NewLine);", endText).AppendLine();
            code.AppendLine(@"return ToString();");
        }

        private void IncludeProperties(StringBuilder code)
        {
            List<BaseDirective> propertyDirectives;
            if (!_directives.TryGetValue(typeof (PropertyDirective), out propertyDirectives))
                return;
            foreach (PropertyDirective propertyDirective in propertyDirectives)
            {
                code.AppendFormat("private {0} _{1}", propertyDirective.Type, propertyDirective.Name);
                if (!string.IsNullOrEmpty(propertyDirective.Default))
                    code.AppendFormat(@" = BaseGenerator.FromString<{0}>(@""{1}"")", propertyDirective.Type, propertyDirective.Default);
                code.AppendLine(";");
                code.AppendFormat("public {0} {1}", propertyDirective.Type, propertyDirective.Name).AppendLine();
                code.AppendLine("{");
                code.AppendFormat("get {{ return _{0}; }}", propertyDirective.Name).AppendLine();
                code.AppendFormat("set {{ _{0} = value; }}", propertyDirective.Name).AppendLine();
                code.AppendLine("}");
            }
        }

        private const string DirectiveTypeNameFormat = "dotGen.{0}Directive, dotGen";

        private const string BaseGeneratorResource = "dotGen.BaseGenerator.cs";

        private static readonly Regex DirectiveRE = new Regex(@"^<%@\s*(?<directive>\w+)(?:\s+(?<names>\w+)=""(?<values>[\w\p{P}]+)"")*\s*%>$");
        private const string DirectiveRE_Directive = "directive";
        private const string DirectiveRE_Names = "names";
        private const string DirectiveRE_Values = "values";

        private static readonly Regex ScriptBlockRE = new Regex(@"<script runat=""template"">(?<block>.*)<\/script>", RegexOptions.Singleline);
        private const string ScriptBlockRE_CodeBlock = "block";
    }
}
